<?php
/**
 * @author     Doug Burns <dougb@arc90.com>
 * @copyright  Copyright (c) 2009 {@link http://arc90.com Arc90 Inc.}
 * @license    http://opensource.org/licenses/bsd-license.php
 */

/**
 * Software License Agreement (BSD License)
 * 
 * Copyright (c) 2009, Arc90 Inc.
 * All rights reserved.
 * 
 * Redistribution and use of this software in source and binary forms, with or
 * without modification, are permitted provided that the following conditions
 * are met:
 * 
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * 
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * - Neither the name of Arc90 Inc. nor the names of its contributors may be
 *   used to endorse or promote products derived from this software without
 *   specific prior written permission of Arc90 Inc.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

require_once('Arc90/Service/Yammer.php');
require_once('Arc90/Service/Twitter.php');

class Yeller
{
	const STATE_FILE = 'state.json';
	const MAX_REQUESTS = 10;
	const WAIT_TIME = 30;
	const MAX_MESSAGE_LENGTH = 140;
	const MAX_RESPONSE_COUNT = 20;
	
	private $yammer;
	private $twitter;
	
	/**
	 * Construct a Yeller instance. If there's a state file present, initialize using it.
	 */
	public function __construct()
	{
		$state = $this->readState();
		if($state != null)
		{
			$this->init($state);
		}
	}
	
	/**
	 * Create yammer and twitter instances using the credentials in the state array.
	 *
	 * @param array $state The state array
	 */
	private function init($state)
	{
		$yammerUsername = $state['credentials']['yammer']['username'];
		$yammerPassword = $state['credentials']['yammer']['password'];
		$twitterUsername = $state['credentials']['twitter']['username'];
		$twitterPassword = $state['credentials']['twitter']['password'];
		
		$this->yammer = new Arc90_Service_Yammer($yammerUsername, $yammerPassword);
		$this->twitter = new Arc90_Service_Twitter($twitterUsername, $twitterPassword);
	}
	
	/**
	 * Create the state file. Save yammer/twitter credentials, public tag name and signature template into it.
	 *
	 * @param string $yammerUsername The username to use for yammer access
	 * @param string $yammerPassword The password to use for yammer access
	 * @param string $twitterUsername The username to use for twitter access
	 * @param string $twitterPassword The password to use for twitter access
	 * @param string $tag The hash tag (for example #public) that designates public messages
	 * @param string $signature A signature template - example [By ?]
	 */
	public function setup($yammerUsername, $yammerPassword, $twitterUsername, $twitterPassword, $tag, $signature)
	{
		$state = array();
		$state['credentials'] = array(
			'yammer' => array('username' => $yammerUsername, 'password' => $yammerPassword),
			'twitter' => array('username' => $twitterUsername, 'password' => $twitterPassword)
		);
		
		$state['tag'] = $tag;
		$state['signature'] = $signature;
		
		$this->init($state);
		$this->writeState($state);
	}
	
	/**
	 * Move tagged yammer messages to twitter and replies to yammer.
	 */
	public function run()
	{
		$messages = $this->getPublicYammerMessages();
		$this->postTwitterMessages($messages);
		
		$replies = $this->getTwitterReplies();
		$this->postYammerReplies($replies);
	}
	
	/**
	 * Get an array of yammer messages that include the public tag.
	 *
	 * @return array All of the new yammer messages
	 */
	private function getPublicYammerMessages()
	{
		$state = $this->readState();
		$messages = array();
		
		if(array_key_exists('last_message', $state) && strlen(trim($state['last_message'])))
		{
			$count = 0;
			$olderThan = '';
			// Infinite loop protection
			while($count < self::MAX_REQUESTS)
			{
				$params = array('newer_than' => $state['last_message']);
				$response = $this->yammer->getMessagesFollowing('json', $params);
				if($response->isError)
				{
					throw new Exception('Error getting yammer messages. ' . $response);
				}
				if(sizeof($response->messages) > 0)
				{
					// Every time there is a request update $olderThan with the first (newest) message id
					$olderThan = $response->messages[0]->id;
					foreach($response->messages as $message)
					{
						// If the public tag is found in the message then add it to the output
						if(strpos($message->body->plain, $state['tag']))
						{
							array_push($messages, $message);
						}
					}
					$count++;
					
					// If response didn't contain the max number of messages then there is nothing else to get.
					if(sizeof($messages) < self::MAX_RESPONSE_COUNT)
					{
						break;
					}
					else
					{
						// Pause, then request the next page of results
						sleep(self::WAIT_TIME);
					}
				}
				else
				{
					break;
				}
			}
			// If older than has been set then update the last message id in the state file
			if(strlen(trim($olderThan)))
			{
				$state['last_message'] = $olderThan;
			}
		}
		else
		{
			// On the first request just set last_message to the most recent message id
			$items = $this->yammer->getMessagesAll()->messages;
			$state['last_message'] = $items[0]->id;
		}
		
		$this->writeState($state);
		
		return $messages;
	}
	
	/**
	 * Posts an array of twitter messages to yammer. 
	 * 
	 * @param $messages An array of the yammer messages
	 */
	private function postTwitterMessages($messages)
	{
		$state = $this->readState();
		
		// Process the yammer messages
		if(sizeof($messages))
		{
			foreach($messages as $message)
			{
				$text = trim($message->body->plain);
				$tag = $state['tag'];
				$id = $message->id;
				
				// Remove the public tag. First check the middle of string, then begin, end, or up against a char
				$text = preg_replace("/\s{$tag}\s/", ' ', $text);
				$text = preg_replace("/^{$tag}\s*|\s*{$tag}$|\s*{$tag}\s*/", '', $text);
				$text = trim($text);

				// Lookup the yammer username using the sender id
				if($name = $state['users'][$message->sender_id])
				{
					// See if there's an associated twitter username
					if($twitterName = $state['associations'][$name])
					{
						$name = "@{$twitterName}";
					}

					// Create the author block
					$authorBlock = str_replace('?', $name, $state['signature']);

					// Truncate the message if necessary to get everything into MAX_MESSAGE_LENGTH chars
					if(strlen("{$text} ${authorBlock}") > self::MAX_MESSAGE_LENGTH)
					{
						$blockLength = strlen(" ${authorBlock}");
						$text = substr($text, 0, self::MAX_MESSAGE_LENGTH - $blockLength);
					}

					// Create the final message
					$text = "{$text} {$authorBlock}";

					// Post to twitter
					$response = $this->twitter->updateStatus($text);
					if($response->isError())
					{
						throw new Exception('Error posting twitter update. ' . $response);
					}
					sleep(self::WAIT_TIME);
				}
				else
				{
					continue;
				}
			}
		}
	}
	
	/**
	 * Gets all twitter replies since the last request. If no previous request was made
	 * then gets the most recent 20 replies.
	 *
	 * @return array $replies An array of twitter replies
	 */
	private function getTwitterReplies()
	{
		$state = $this->readState();
		$page = 1;
		$replies = array();
		$lastId = '';
		
		if(array_key_exists('last_reply', $state) && strlen(trim($state['last_reply'])))
		{
			$lastId = $state['last_reply'];
			
			// Infinite loop prevention
			while($count < self::MAX_REQUESTS)
			{
				$response = $this->twitter->getReplies('json', $page, array('since_id' => $lastId));
				if($response->isError())
				{
					throw new Exception('Error getting twitter replies. ' . $response);
				}
				
				$items = json_decode($response->getData());

				foreach($items as $item)
				{
					array_push($replies, $item);
				}

				if(sizeof($items) < self::MAX_RESPONSE_COUNT)
				{
					break;
				}
				else
				{
					$lastId = $items[0]->id;
					$page++;
				}
			}
		}
		else
		{
			$response = $this->twitter->getReplies('json', 0);
			if($response->isError())
			{
				throw new Exception('Error getting twitter replies. ' . $response);
			}
			
			$items = json_decode($response->getData());
			$lastId = $items[0]->id;
		}
		
		if(sizeof($replies))
		{
			$lastId = end($replies)->id;
		}
		
		if(strlen(trim($lastId)))
		{
			$state['last_reply'] = $lastId;
			$this->writeState($state);
		}
		
		return $replies;
	}
	
	/**
	 * Posts replies from twitter to yammer
	 *
	 * @param array $replies An array of twitter replies to post 
	 */
	private function postYammerReplies($replies)
	{
		foreach($replies as $reply)
		{
			$response = $this->yammer->postMessage($reply->text);
			if($response->isError())
			{
				throw new Exception('Error posting reply to yammer. ' . $response);
			}
			sleep(self::WAIT_TIME);
		}
	}
	
	/**
	 * Associate a twitter and yammer account
	 *
	 * @param string $yammerName The user's yammer username
	 * @param string $twitterName The user's twitter username
	 * @return boolean True if the yammer user exists, false otherwise (needs refresh)
	 */
	public function associate($yammerName, $twitterName)
	{
		$state = $this->readState();
		
		if(!is_array($state['associations']))
		{
			$state['associations'] = array();
		}
		$state['associations'][$yammerName] = $twitterName;
		
		$this->writeState($state);
		
		if(is_array($state['users']) && array_search($yammerName, $state['users']))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	/**
	 * Remove the association between a twitter and yammer account
	 * 
	 * @param string $yammerName The user's yammer username
	 */
	public function disassociate($yammerName)
	{
		$state = $this->readState();
		
		if(is_array($state['associations']))
		{
			unset($state['associations'][$yammerName]);
		}
		
		$this->writeState($state);
	}
	
	/**
	 * Get all users from yammer and caches their id/name pairs in the state file.
	 */
	public function updateUsers()
	{
		$state = $this->readState();
		$state['users'] = array();
		$page = 1;
		
		// Infinite loop prevention
		while($page < self::MAX_REQUESTS)
		{
			$response = $this->yammer->getUsers('json', $page);
			if($response->isError())
			{
				throw new Exception('Error getting yammer users. ' . $response);
			}
			$users = $response->getDataObject();
			if(sizeof($users) > 0)
			{
				foreach($users as $user)
				{
					$state['users'][$user->id] = $user->name;
				}
				$page++;
				sleep(self::WAIT_TIME);
			}
			else
			{
				break;
			}
		}
		
		$this->writeState($state);
	}
	
	/**
	 * Converts state array to json and writes it to file. Locks the file by creating a 
	 * lock file before writing. Once complete, that file is deleted.
	 *
	 * @param array $state The state array
	 */
	private function writeState($state)
	{
		$lockFile = self::STATE_FILE . '.lock'; 
		if(file_exists($lockFile))
		{
			throw new Exception('Cannot write to state file. The file is locked.');
		}
		touch($lockFile);
		try
		{
			$json = json_encode($state);
			file_put_contents(self::STATE_FILE, $json);
		}
		catch(Exception $e)
		{
		}
		unlink($lockFile);
	}
	
	/**
	 * Reads json from state file, converts it to an array, and returns the array.
	 * Throws an exception if the state file is locked for writing.
	 *
	 * @return array The state array
	 */
	private function readState()
	{
		if(file_exists(self::STATE_FILE . '.lock'))
		{
			throw new Exception('Cannot read state file. The file is locked.');
		}
		if(file_exists(self::STATE_FILE))
		{
			$json = file_get_contents(self::STATE_FILE);
			$array = json_decode($json, true);
			return $array;
		}
		else
		{
			return null;
		}
	}
}

/**
 * Command line interface for interacting with the Yeller class
 */
$command = $argv[1];
if(!$command)
{
	die("Please provide a command as an argument.\nValid commands are 'setup', 'run', 'associate', 'disassociate'.\n");
}

$yeller = new Yeller();

switch($command)
{
	case 'run':
		$yeller->run();
		break;
	case 'associate':
		print "This will associate a Yammer account with a Twitter account.\n";
		print "What's the Yammer username?: ";
		$yammerUsername = trim(fgets(STDIN));
		print "What's the Twitter username?: ";
		$twitterUsername = trim(fgets(STDIN));
		$exists = $yeller->associate($yammerUsername, $twitterUsername);
		if($exists)
		{
			print "Association created!\n";
		}
		else
		{
			print "The Yammer username {$yammerUsername} doesn't exist in the user cache.\n";
			print "Reloading Yammer users (this may take a bit)...\n";
			$yeller->updateUsers();
			print "Done!\n";
		}
		break;
	case 'disassociate':
		print "This will remove the association between a Yammer account and Twitter account.\n";
		print "What's the Yammer username?: ";
		$yammerUsername = trim(fgets(STDIN));
		$yeller->disassociate($yammerUsername);
		print "Done!\n";
		break;
	case 'setup':
		print "This will overwrite your current configuration including all yammer/twitter associations.\n";
		print 'Do you want to continue? (y/n): ';
		$overwrite = trim(fgets(STDIN));
		if($overwrite != 'y')
		{
			die('Exited without overwriting existing configuration.');
		}
		print "Yeller needs a Yammer account and Twitter account for getting/posting messages.\n";
		print "What's the Yammer username?: ";
		$yammerUsername = trim(fgets(STDIN));
		print "What's the Yammer password?: ";
		$yammerPassword = trim(fgets(STDIN));
		print "What's the Twitter username?: ";
		$twitterUsername = trim(fgets(STDIN));
		print "What's the Twitter password?: ";
		$twitterPassword = trim(fgets(STDIN));
		print "What tag should designate external messages (leave blank for #yell): ";
		$tag = trim(fgets(STDIN));
		if($tag == '')
		{
			$tag = '#yell';
		}
		print "What format would you like to use for the message signature? (leave blank for [By ?]): ";
		$signature = trim(fgets(STDIN));
		if($signature == '')
		{
			$signature = '[By ?]';
		}
		print "Saving account info...\n";
		$yeller->setup($yammerUsername, $yammerPassword, $twitterUsername, $twitterPassword, $tag, $signature);
		print "Caching Yammer users (this may take a bit)...\n";
		$yeller->updateUsers();
		print "Done! Now just create a cron job or scheduled task that executes 'php Yeller.php run'.\n";
		break;
	default:
		die("Unsupported command '{$command}'. valid command are 'setup', 'run', 'associate', 'disassociate'");
}
?>